Python3によるオブジェクト指向プログラミング - クラスとメンバー
著者:Leonardo Giordani - 20/08/2014 Updated on May 22, 2019
この記事はIPython Notebookとして公開 されています。 Pythonのクラスの再登場
Pythonのクラスの実装には、いくつかの特殊性があります。素直に言うと、Pythonではオブジェクトのクラスはオブジェクトそのものです。これは、クラスを type() で調べることで確認できます。
code: python
>> a = 1
>> type(a)
<class 'int'>
>> type(int)
<class 'type'>
これはintクラスがオブジェクトであり、type`クラスのインスタンスであることを示しています。
この概念は、一見するとそれほど難しいものではありません。現実の世界では、概念を物のように使って扱います。例えば、「ドア」という概念について、ドアがどのような形をしていて、どのように機能するかを人々に話します。この場合、ドアという概念が議論の対象となります。つまり、私たちの日常的な経験では、オブジェクトのタイプはオブジェクトそのものなのです。Pythonでは、これを「すべてがオブジェクトである」と表現できます。
オブジェクトのクラス自体がインスタンスであれば、それは具象オブジェクトであり、メモリのどこかに保存されています。オブジェクトの状態を確認するために、Pythonの検査機能(inspection capabilities) とid() 関数を利用してみましょう。id() 組み込み関数は、オブジェクトのメモリ上の位置を返します。
最初の記事では、このクラスを定義しました
code: python
class Door:
def __init__(self, number, status):
self.number = number
self.status = status
def open(self):
self.status = 'open'
def close(self):
self.status = 'closed'
まず、Doorクラスのインスタンスを2つ作成し、2つのオブジェクトが異なるアドレスに格納されていることを確認します。
code: python
>> door1 = Door(1, 'closed')
>> door2 = Door(1, 'closed')
>> hex(id(door1))
'0xb67e148c'
>> hex(id(door2))
'0xb67e144c'
これにより、2つのインスタンスが独立しており、無関係であることが確認されました。あなたの値は、私が得た値とは非常に異なる可能性があることに注意してください。メモリアドレスは実行のたびに変化するからです。2つ目のインスタンスには1つ目のインスタンスと同じ属性が与えられており、属性の値に関わらず2つのインスタンスが異なるオブジェクトであることを示しています。
しかし、2つのインスタンスのクラスに id() を使用すると、クラスが全く同じであることがわかります。
code: python
>> hex(id(door1.__class__))
'0xb685f56c'
>> hex(id(door2.__class__))
'0xb685f56c'
これは非常に重要なことです。Pythonでは、クラスはオブジェクトを構築するための単なるスキーマではありません。むしろ、クラスは共有された生きたオブジェクトであり、そのコードはランタイムにアクセスされます。
しかし、すでにテストしたように、__init__() がクラスを生成する際にself に作用するため、属性はクラスには保存されず、すべてのインスタンスに保存されます。しかし、クラスは他のオブジェクトのように属性を与えることができます。想像力を働かせて、これをクラス属性と呼びましょう。
ご存知のように、クラス属性は、コンテナと同じようにクラスインスタンスの間で共有されます。
code: python
class Door:
colour = 'brown'
def __init__(self, number, status):
self.number = number
self.status = status
def open(self):
self.status = 'open'
def close(self):
self.status = 'closed'
注意:ここでの colour属性は、self を使用して作成されていないため、クラスに含まれており、インスタンス間で共有されています。
code: python
>> door1 = Door(1, 'closed')
>> door2 = Door(2, 'closed')
>> Door.colour
'brown'
>> door1.colour
'brown'
>> door2.colour
'brown'
ここまでは先ほどのケースと変わりません。共有値の変更がすべてのインスタンスに反映されるかどうか見てみましょう。
code: python
>> Door.colour = 'white'
>> Door.colour
'white'
>> door1.colour
'white'
>> door2.colour
'white'
>> hex(id(Door.colour))
'0xb67e1500'
>> hex(id(door1.colour))
'0xb67e1500'
>> hex(id(door2.colour))
'0xb67e1500'
レイダース/失われた属性
すべてのPythonオブジェクトには、自動的に __dict__ 属性が与えられ、それは属性のリストを含んでいます。例題のオブジェクトについて、この辞書が何を含んでいるか調べてみましょう。
code: python
>> Door.__dict__
mappingproxy({'open': <function Door.open at 0xb68604ac>,
'colour': 'brown',
'__dict__': <attribute '__dict__' of 'Door' objects>,
'__weakref__': <attribute '__weakref__' of 'Door' objects>,
'__init__': <function Door.__init__ at 0xb7062854>,
'__module__': '__main__',
'__doc__': None,
'close': <function Door.close at 0xb686041c>})
>> door1.__dict__
{'number': 1, 'status': 'closed'}
dictとMappingProxyTypeオブジェクトの違いはさておき、color属性はDoorクラスの属性としてリストアップされており、status と number はインスタンスとしてリストアップされていることがわかります。
その属性がそのインスタンスにリストされていないのに、どうして door1.color を呼び出すことができるのでしょうか?これは魔法の __getattribute__() メソッドが行う仕事です。Pythonではドット構文が自動的にこのメソッドを呼び出すので、door1.color と書くとPythonは door1.__getattribute__('color') を実行します。このメソッドは属性の検索を行います。
標準的な __getattribute__() の実装では、まずオブジェクトの内部辞書 (__dict__) を検索し、次にオブジェクト自体の型を検索します。この場合、door1.__getattribute__('color') はまず door1.__dict__['color'] を実行し、次に後者が KeyError 例外を発生させるので、door1.__class__.__dict__['color'] を実行します。
code: python
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'colour'
'brown'
実際、is 演算子を使ってオブジェクトの同一性を比較すると、door1.color と Door.color の両方が全く同じオブジェクトであることが確認できます。
code: python
>> door1.colour is Door.colour
True
インスタンスのクラス属性に直接値を割り当てようとすると、インスタンスの __dict__ にその名前の値を入れるだけで、__getattribute__() で最初に見つかるので、この値はクラス属性をマスクしてしまいます。前節の例からもわかるように、これはクラス自体の属性の値を変更するのとは違います。
code: python
>> door1.colour = 'white'
'white'
'brown'
>> Door.colour = 'red'
'white'
'red'
メソッドの復習
同じゲームをメソッドについてもやってみましょう。まず最初に、クラスの属性と同様に、メソッドもクラスの __dict__ にのみ記載されていることがわかります。メソッドを取得すると、属性と同じように動作する可能性があります。
code: python
>> door1.open is Door.open
False
あらら。さらに調べてみるとみましょう。
code: python
<function Door.open at 0xb68604ac>
>> Door.open
<function Door.open at 0xb68604ac>
>> door1.open
<bound method Door.open of <__main__.Door object at 0xb67e162c>>
そのため、クラスメソッドはメンバーズ辞書にfunctionとして記載されています。ここまでは順調です。ここでPython 2はPython 3には存在しないunbound()メソッドを導入する必要がありました。インスタンスから取得すると、バインドされたメソッドが返されます。
さて、関数はdef文で名前を付けて定義した手続きです。Python 3でクラスの一部として関数を参照すると、クラスの外で定義された関数と何の違いもない、プレーンな関数が得られます。
しかし、インスタンスから関数を取得した場合、それはバインドされたメソッドになります。メソッドという名前は、通常のOOPの定義によれば、単純に「オブジェクト内部の関数」を意味し、バインドはメソッドがそのインスタンスにリンクされていることを示します。なぜPythonはメソッドがバインドされているかどうかを気にするのでしょうか?また、Pythonはどのようにして関数をバインドされたメソッドに変換するのでしょうか?
まず最初に、クラスの関数を呼ぼうとすると、以下のようなエラーが発生します。
code: python
>> Door.open()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: open() missing 1 required positional argument: 'self'
確かに、この関数は selfという引数を必要とするように定義されていて、引数なしで呼び出すと例外が発生します。これはおそらく、クラスのインスタンスを1つ与えれば動作することを意味しています。
code: python
>> Door.open(door1)
>> door1.status
'open'
Pythonはここではエラーにならず、このメソッドは期待通りに動作します。つまり、Door.open(door1) は door1.open() と同じであり、これがクラスから来た普通の関数とバインドされたメソッドの違いです:バインドされたメソッドは自動的にインスタンスを関数の引数として渡します。
繰り返しになりますが、内部処理では __getattribute__() がすべてを動作させるために働いており、door1.open() を呼び出すとき、Python は実際に door1.__class__.open(door1) を呼び出します。しかし、door1.__class__.openは普通の関数なので、Pythonが安全に呼べるバインドメソッドに変換するための何かがあります。
オブジェクトのメンバーにアクセスするとき、Pythonはその要求を満たすために __getattribute__() を呼び出します。しかし、このマジックメソッドは、ディスクリプタプロトコル(Descriptor Protocol) として知られる手順に準拠しています。読み込みアクセスの場合、__getattribute__() は、オブジェクトが __get__() メソッドを持っているかどうかをチェックし、後者を呼び出します。つまり、関数がバインドされたメソッドに変換されるのは、このようなメカニズムによって起こるのです。例を挙げて説明しましょう。
code: python
>> door1.__class__.__dict__'open' <function Door.open at 0xb68604ac>
この構文は、クラスで定義されている関数を取得します。この関数は、オブジェクトについて何も知りませんが、オブジェクトです(「すべてはオブジェクトである」ことを覚えておいてください)。ですから,組み込み関数のdir()を使って,その内部を見ることができます
code: python
>> dir(door1.__class__.__dict__'open') ['__annotations__', '__call__', '__class__', '__closure__', '__code__',
'__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__',
'__format__', '__ge__', '__get__', '__getattribute__', '__globals__',
'__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__',
'__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__']
>> door1.__class__.__dict__'open'.__get__ <method-wrapper '__get__' of function object at 0xb68604ac>
見ての通り、関数のメンバーの中に __get__ メソッドが含まれており、Python はこれをメソッドラッパーとして認識しています。このメソッドは、open()関数をdoor1インスタンスに接続し、インスタンスだけを渡して呼び出すことができます。
code: python
>> door1.__class__.__dict__'open'.__get__(door1) <bound method Door.open of <__main__.Door object at 0xb67e162c>>
上記のように入力すると、探していたものが正確に得られます。この複雑な構文は、インスタンスのメソッドを呼び出すときに内部で起こっていることです。
メソッドとクラスの出会い
クラス内で定義された関数に type() を使用すると、クラスの内部表現に関する他の詳細がわかります。
code: python
>> Door.open
<function Door.open at 0xb687e074>
>> door1.open
<bound method Door.open of <__main__.Door object at 0xb6f9834c>>
>> type(Door.open)
<class 'function'>
>> type(door1.open)
<class 'method'>
ご覧の通り、Pythonはこの2つを区別して、1つ目を関数、2つ目をメソッドと認識しています。2つ目はインスタンスにバインドされた関数です。
もし、インスタンスを操作するのではなく、クラスを操作する関数を定義したい場合はどうすればよいでしょうか?クラスの属性を定義するのと同様に、Pythonではclassmethodデコレーターを使ってクラスメソッドを定義することができます。クラスメソッドは、インスタンスではなくクラスに束縛される関数です。
code: python
class Door:
colour = 'brown'
def __init__(self, number, status):
self.number = number
self.status = status
@classmethod
def knock(cls):
print("Knock!")
def open(self):
self.status = 'open'
def close(self):
self.status = 'closed'
このように定義することで、インスタンスとクラスの両方でメソッドを呼び出すことができます。
code: python
>> door1.knock()
Knock!
>> Door.knock()
Knock!
そして、Pythonは両方とも(バインドされた)メソッドとして識別します。
code: python
<classmethod object at 0xb67ff6ac>
>> door1.knock
<bound method type.knock of <class '__main__.Door'>>
>> Door.knock
<bound method type.knock of <class '__main__.Door'>>
>> type(Door.knock)
<class 'method'>
>> type(door1.knock)
<class 'method'>
ご覧の通り、knock() 関数は1つの引数を受け取ります。この引数はインスタンスではなく、クラスそのものであることを忘れないように、cls と呼ばれます。つまり、関数の内部ではクラスを操作することができ、クラスはインスタンス間で共有されています。
code: python
class Door:
colour = 'brown'
def __init__(self, number, status):
self.number = number
self.status = status
@classmethod
def knock(cls):
print("Knock!")
@classmethod
def paint(cls, colour):
cls.colour = colour
def open(self):
self.status = 'open'
def close(self):
self.status = 'closed'
paint()クラスメソッドは、インスタンス間で共有されるクラス属性の色を変更するようになりました。動作を確認してみましょう。
code: python
>> door1 = Door(1, 'closed')
>> door2 = Door(2, 'closed')
>> Door.colour
'brown'
>> door1.colour
'brown'
>> door2.colour
'brown'
>> Door.paint('white')
>> Door.colour
'white'
>> door1.colour
'white'
>> door2.colour
'white'
クラスメソッドはクラス上で呼び出すことができますが、インスタンスのカラー属性は実行時に共有クラスから取得されるため、これはクラスとインスタンスの両方に影響します。
code: python
>> door1.paint('yellow')
>> Door.colour
'yellow'
>> door1.colour
'yellow'
>> door2.colour
'yellow'
しかし、クラスメソッドはインスタンスに対しても呼び出すことができ、その効果は以前と同じです。クラスメソッドはクラスに束縛されているので、呼び出した実際のオブジェクト(クラスやインスタンス)に関わらず、後者に対して作用します。
映画のトリビア
セクションのタイトルは、
The Empire Strikes Back (1980): Python Classes Strike Again / Pythonのクラスの再登場
この映画タイトルの邦題は「帝国の逆襲」
Raiders of the Lost Ark (1981): Raiders of the Lost Attribute / レイダース/失われた属性
この映画タイトルの邦題は「レイダース/失われたアーク《聖櫃》」
Revenge of the Nerds (1984): Revenge of the Methods / メソッドの復習
Revenge をどう扱うか考えたのですが、内容と韻を踏んで「復習」としています。
この映画タイトルの邦題は「ナーズの復讐」
When Harry Met Sally (1989): When Methods met Classes / メソッドとクラスの出会い
この映画タイトルの邦題は「恋人たちの予感」で、かなりの超訳です。
参考資料
Python3によるオブジェクト指向プログラミングシリーズ
日本語訳: Python3によるオブジェクト指向プログラミング - クラスとメンバー